Computers / Programming / How-To / MFC / Have a CTabView without Arrow Buttons

Maybe you are trying to build an MFC application to display some information. Maybe you, like I did, realize that there are multiple useful ways to display that information. You, again like me, might think it would be nice if you could display that information in a series of tabs. You do some research and discover the CTabView class which has an AddView() method that looks to do exactly what you want. It promises to allow you to have a set of tabs that each link to separate views. So you derive your main view class from CTabView, fixing up all the references to the old base class, then you create some other view items, and then you hit the first hurdle.

Where should you call AddView()? The constructor? PreCreateWindow? The documentation doesn't say. The example just shows a header file and it seems impossible to get the sample application mentioned. Now this part I'm not sure about but calling AddView() in the OnCreate() method seems to work. It's fairly easy to add a handler for the WM_CREATE message with Class Wizard, as long as Visual Studio hasn't removed that functionality. So you add the OnCreate() method to handle the WM_CREATE message, add your calls to AddView() there, and you should get something like this.

int CTabbedInfoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTabView::OnCreate(lpCreateStruct) == -1)
return -1;
AddView(RUNTIME_CLASS(CView1), TEXT("View 1"));
AddView(RUNTIME_CLASS(CView2), TEXT("View 2"));
return 0;
}

It compiles and you get tabs and everything is awesome. Now your first question might be "Why are the tabs at the bottom?" Well that's easy, it's configurable. You just have to call the SetLocation() method on the underlying CMFCTabCtrl and give it a value from the CMFCBaseTabCtrl::Location enum. Use the GetTabControl() methond to get a reference to the CMFCTabCtrl. After doing that your OnCreate() method may look like this.

int CTabbedInfoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTabView::OnCreate(lpCreateStruct) == -1)
return -1;
AddView(RUNTIME_CLASS(CView1), TEXT("View 1"));
AddView(RUNTIME_CLASS(CView2), TEXT("View 2"));
GetTabControl().SetLocation(CMFCBaseTabCtrl::Location::LOCATION_TOP);
return 0;
}

Now your tabs are at the top and life is good, except what's with these buttons? Maybe that's configurable too. So you look at the documentation for CTabView, CMFCTabCtrl, and CMFCBaseTabCtrl and the best you can find is ModifyTabStyle. So you call that and pass in one of the styles mentioned and you still have weird arrow buttons. You try another, still arrow buttons. You try them all and they all have arrow buttons. What the hell?

MFC Tab Control Styles

Now you might go and do a google search and if your luck is as good as mine you won't find anything. Apparently no one else is worried about these arrow buttons or they all know how to deal with them and feel it's not important to tell anyone. Hopefully you are reading this because you found it in a google search. If that's the case, great, let me help you.

Just to spoil everything the problem is the way that the CTabView::OnCreate() method creates the CMFCTabCtrl control and the way the CMFCTabCtrl control initializes itself. It turns out that there are some style characteristics that are only set when the control is created and that ModifyTabStyle() doesn't change them. The trick is to not call CTabView::OnCreate() and to create the CMFCTabCtrl control yourself. This allows you to specify the style you want in the first place and avoid setting all those style characteristics you don't want. Skip to the end for the solution.

But before we go there I want to talk about how I figured this out because I think understanding why a solution works is very important. Now the main question is where are those buttons created and what controls their creation? Well let's step into the CTabView::OnCreate() method and see what it does. According to my version of the MFC source files included with Visual Studio the CTabView::OnCreate() method  looks like this. You should be able to find your version by debugging the application and stepping into the CTabView::OnCreate() method.

int CTabView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
CRect rectDummy;
rectDummy.SetRectEmpty();
// Create tabs window:
if (!m_wndTabs.Create(IsScrollBar () ? CMFCTabCtrl::STYLE_FLAT_SHARED_HORZ_SCROLL : CMFCTabCtrl::STYLE_FLAT, rectDummy, this, 1))
{
TRACE0("Failed to create tab window\n");
return -1; // fail to create
}
m_wndTabs.SetFlatFrame();
m_wndTabs.SetTabBorderSize(0);
m_wndTabs.AutoDestroyWindow(FALSE);
return 0;
}

The interesting part here is the call to create the control, notice that we are passing in one of two styles. Now you might think well I just need to override IsScrollBar() since it is virtual, right?. Sadly no. Let's step into the CMFCTabCtrl::Create() and see why.

BOOL CMFCTabCtrl::Create(Style style, const RECT& rect, CWnd* pParentWnd, UINT nID, Location location /* = LOCATION_BOTTOM*/, BOOL bCloseBtn /* = FALSE */)
{
m_bFlat = (style == STYLE_FLAT) ||(style == STYLE_FLAT_SHARED_HORZ_SCROLL);
m_bSharedScroll = style == STYLE_FLAT_SHARED_HORZ_SCROLL;
m_bIsOneNoteStyle = (style == STYLE_3D_ONENOTE);
m_bIsVS2005Style = (style == STYLE_3D_VS2005);
m_bLeftRightRounded = (style == STYLE_3D_ROUNDED || style == STYLE_3D_ROUNDED_SCROLL);
m_bHighLightTabs = m_bIsOneNoteStyle;
m_location = location;
m_bScroll = (m_bFlat || style == STYLE_3D_SCROLLED || style == STYLE_3D_ONENOTE || style == STYLE_3D_VS2005 || style == STYLE_3D_ROUNDED_SCROLL);
m_bCloseBtn = bCloseBtn;
if (!m_bFlat && m_bSharedScroll)
{
//--------------------------------------
// Only flat tab has a shared scrollbar!
//--------------------------------------
ASSERT(FALSE);
m_bSharedScroll = FALSE;
}
return CMFCBaseTabCtrl::Create(GetGlobalData()->RegisterWindowClass(_T("Afx:TabWnd")), _T(""), WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, rect, pParentWnd, nID);
}

If you search through the source for CMFCTabCtrl you will find out two things. The first is that m_bScroll controls the creation of the buttons and the second is that m_bScroll is only set in the constructor and in the Create() method. So to hide the buttons we want m_bScroll to be false so when is that? Well we can see that m_bScroll is true if the style is STYLE_3D_SCROLLED, STYLE_3D_ONENOTE, STYLE_3D_VS2005 or STYLE_3D_ROUNDED_SCROLL or if m_bFlat is true. m_bFlat is true when the style is STYLE_FLAT or STYLE_FLAT_SHARED_HORZ_SCROLL. So the only styles without buttons are STYLE_3D and STYLE_3D_ROUNDED. Going back to CTabView::OnCreate() you see that the two style options both have buttons. So changing IsScrollBar() doesn't change the problem.

Well if CTabView::OnCreate() isn't doing what we want let's just skip over it. Copy out the code, change all references to m_wndTabs to be GetTabControl() and set the style you want. Something like this.

int CTabbedInfoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
CRect rectDummy;
rectDummy.SetRectEmpty();
// Create tabs window:
if (!GetTabControl().Create(CMFCTabCtrl::STYLE_3D_ROUNDED, rectDummy, this, 1))
{
TRACE0("Failed to create tab window\n");
return -1; // fail to create
}
GetTabControl().SetFlatFrame();
GetTabControl().SetTabBorderSize(0);
GetTabControl().AutoDestroyWindow(FALSE);
AddView(RUNTIME_CLASS(CView1), TEXT("View 1"));
AddView(RUNTIME_CLASS(CView2), TEXT("View 2"));
GetTabControl().SetLocation(CMFCBaseTabCtrl::Location::LOCATION_TOP);
return 0;
}

And behold, no more buttons. The nice thing about MFC is that it includes the source so you can usually figure out what it's doing and how to get it to do what you want.